﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Terminal.cs" company="Sitecore A/S">
//   Copyright (C) by Sitecore A/S
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace AurelienRibon.Ui.Terminal
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Media;
    using System.Windows.Controls;
    using System.Windows.Input;

    /// <summary>The terminal.</summary>
    public class Terminal : TextBox
    {
        #region Constants and Fields

        /// <summary>The index in log.</summary>
        private int indexInLog = 0;
        private int lastSuggestionIndex = -1;
        private string lastAutocompleteLine = null;
        #endregion

        #region Constructors and Destructors

        /// <summary>Initializes a new instance of the <see cref="Terminal"/> class.</summary>
        public Terminal()
        {
            this.IsUndoEnabled = false;
            this.AcceptsReturn = false;
            this.AcceptsTab = false;

            this.RegisteredCommands = new List<string>();
            this.CommandLog = new List<Command>();
            this.IsPromptInsertedAtLaunch = true;
            this.IsSystemBeepEnabled = true;
            this.LastPomptIndex = -1;
            this.Prompt = "> ";
            this.IsInputEnabled = false;

            this.Loaded += (s, e) =>
                               {
                                   if (this.IsPromptInsertedAtLaunch)
                                   {
                                       this.InsertNewPrompt();
                                   }
                               };

            this.TextChanged += (s, e) =>
                                    {
                                        this.ScrollToEnd();
                                    };
        }

        #endregion

        #region Public Events

        /// <summary>The abort requested.</summary>
        public event EventHandler<EventArgs> AbortRequested;

        /// <summary>The command entered.</summary>
        public event EventHandler<CommandEventArgs> CommandEntered;

        /// <summary>The command entered.</summary>
        public event EventHandler<CommandEventArgs> AutoCompleteRequested;

        #endregion

        #region Enums

        /// <summary>The command history direction.</summary>
        protected enum CommandHistoryDirection
        {
            /// <summary>The backward.</summary>
            BACKWARD,

            /// <summary>The forward.</summary>
            FORWARD
        }

        #endregion

        #region Public Properties

        /// <summary>Gets the command log.</summary>
        public List<Command> CommandLog { get; private set; }

        /// <summary>Gets a value indicating whether is input enabled.</summary>
        public bool IsInputEnabled { get; private set; }

        /// <summary>Gets or sets a value indicating whether is prompt inserted at launch.</summary>
        public bool IsPromptInsertedAtLaunch { get; set; }

        /// <summary>Gets or sets a value indicating whether is system beep enabled.</summary>
        public bool IsSystemBeepEnabled { get; set; }

        /// <summary>Gets the last pompt index.</summary>
        public int LastPomptIndex { get; private set; }

        /// <summary>Gets or sets the prompt.</summary>
        public string Prompt { get; set; }

        /// <summary>Gets the registered commands.</summary>
        public List<string> RegisteredCommands { get; private set; }

        #endregion

        #region Public Methods

        /// <summary>The insert line before prompt.</summary>
        /// <param name="text">The text.</param>
        public void InsertLineBeforePrompt(string text)
        {
            var oldPromptIndex = this.LastPomptIndex;
            var insertedText = text + (text.EndsWith("\n") ? string.Empty : "\n");
            this.Text = this.Text.Insert(this.LastPomptIndex - this.Prompt.Length, insertedText);
            this.CaretIndex = this.Text.Length;
            this.LastPomptIndex = oldPromptIndex + insertedText.Length;
        }

        /// <summary>The insert new prompt.</summary>
        public void InsertNewPrompt()
        {
            if (this.Text.Length > 0)
            {
                this.Text += this.Text.EndsWith("\n") ? string.Empty : "\n";
            }

            this.Text += this.Prompt;
            this.CaretIndex = this.Text.Length;
            this.LastPomptIndex = this.Text.Length;
            this.IsInputEnabled = true;
        }

        #endregion

        // --------------------------------------------------------------------
        // EVENT HANDLER
        // --------------------------------------------------------------------

        // --------------------------------------------------------------------
        // CLASS SPECIFIC UTILITIES
        // --------------------------------------------------------------------

        #region Methods

        /// <summary>The complete path.</summary>
        /// <param name="linePrefix">The line prefix.</param>
        /// <param name="lineSuffix">The line suffix.</param>
        protected void CompletePath(string linePrefix, string lineSuffix)
        {
            if (lineSuffix.Contains("\\") || lineSuffix.Contains("/"))
            {
                var idx = Math.Max(lineSuffix.LastIndexOf("\\"), lineSuffix.LastIndexOf("/"));
                var dir = lineSuffix.Substring(0, idx + 1);
                var prefix = lineSuffix.Substring(idx + 1, lineSuffix.Length - dir.Length);
                var files = this.GetFileList(dir, lineSuffix[idx] == '\\');

                var commonPrefixFiles = new List<string>();
                foreach (var file in files)
                {
                    if (file.StartsWith(prefix, StringComparison.CurrentCultureIgnoreCase))
                    {
                        commonPrefixFiles.Add(file);
                    }
                }

                if (commonPrefixFiles.Count > 0)
                {
                    var commonPrefix = this.GetCommonPrefix(commonPrefixFiles.ToArray());
                    if (commonPrefix == prefix)
                    {
                        foreach (var file in commonPrefixFiles)
                        {
                            this.Text += "\n" + file;
                        }

                        this.InsertNewPrompt();
                        this.Text += linePrefix + lineSuffix;
                        this.CaretIndex = this.Text.Length;
                    }
                    else
                    {
                        this.Text = this.Text.Remove(this.LastPomptIndex);
                        this.Text += linePrefix + dir + commonPrefix;
                        this.CaretIndex = this.Text.Length;
                    }
                }
            }
        }

        /// <summary>The get associated commands.</summary>
        /// <param name="prefix">The prefix.</param>
        /// <returns>The System.String[].</returns>
        protected List<string> GetAssociatedCommands(string prefix)
        {
            if (lastSuggestionIndex < 0)
            {
                lastAutocompleteLine = prefix;
                Command cmdLine = TerminalUtils.ParseCommandLine(prefix);
                RegisteredCommands.Clear();
                RegisteredCommands.AddRange(RaiseAutoCompleteRequested(cmdLine));
            }

            return RegisteredCommands;

/*
            var ret = new List<string>();
            foreach (var cmd in this.RegisteredCommands)
            {
                if (cmd.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase))
                {
                    ret.Add(cmd);
                }
            }

            return ret.ToArray();
*/
        }

        // --------------------------------------------------------------------
        // GENERAL UTILITIES
        // --------------------------------------------------------------------

        /// <summary>The get common prefix.</summary>
        /// <param name="strs">The strs.</param>
        /// <returns>The System.String.</returns>
        protected string GetCommonPrefix(string[] strs)
        {
            var shortestStr = this.GetShortestString(strs);
            for (var i = 0; i < shortestStr.Length; i++)
            {
                foreach (var str in strs)
                {
                    if (char.ToLower(str[i]) != char.ToLower(shortestStr[i]))
                    {
                        return shortestStr.Substring(0, i);
                    }
                }
            }

            return shortestStr;
        }

        /// <summary>The get file list.</summary>
        /// <param name="dir">The dir.</param>
        /// <param name="useAntislash">The use antislash.</param>
        /// <returns>The System.String[].</returns>
        protected string[] GetFileList(string dir, bool useAntislash)
        {
            if (!Directory.Exists(dir))
            {
                return new string[0];
            }

            var dirs = Directory.GetDirectories(dir);
            var files = Directory.GetFiles(dir);

            for (var i = 0; i < dirs.Length; i++)
            {
                dirs[i] = Path.GetFileName(dirs[i]) + (useAntislash ? "\\" : "/");
            }

            for (var i = 0; i < files.Length; i++)
            {
                files[i] = Path.GetFileName(files[i]);
            }

            var ret = new List<string>();
            ret.AddRange(dirs);
            ret.AddRange(files);
            return ret.ToArray();
        }

        /// <summary>The get shortest string.</summary>
        /// <param name="strs">The strs.</param>
        /// <returns>The System.String.</returns>
        protected string GetShortestString(string[] strs)
        {
            var ret = strs[0];
            foreach (var str in strs)
            {
                ret = str.Length < ret.Length ? str : ret;
            }

            return ret;
        }

        /// <summary>The get text with prompt suffix.</summary>
        /// <param name="suffix">The suffix.</param>
        /// <returns>The System.String.</returns>
        protected string GetTextWithPromptSuffix(string suffix)
        {
            var ret = this.Text.Substring(0, this.LastPomptIndex);
            return ret + suffix;
        }

        /// <summary>The handle command history request.</summary>
        /// <param name="direction">The direction.</param>
        protected virtual void HandleCommandHistoryRequest(CommandHistoryDirection direction)
        {
            switch (direction)
            {
                case CommandHistoryDirection.BACKWARD:
                    if (this.indexInLog > 0)
                    {
                        this.indexInLog--;
                    }

                    if (this.CommandLog.Count > 0)
                    {
                        this.Text = this.GetTextWithPromptSuffix(this.CommandLog[this.indexInLog].Raw);
                        this.CaretIndex = this.Text.Length;
                    }

                    break;

                case CommandHistoryDirection.FORWARD:
                    if (this.indexInLog < this.CommandLog.Count - 1)
                    {
                        this.indexInLog++;
                    }

                    if (this.CommandLog.Count > 0)
                    {
                        this.Text = this.GetTextWithPromptSuffix(this.CommandLog[this.indexInLog].Raw);
                        this.CaretIndex = this.Text.Length;
                    }

                    break;
            }
        }

        /// <summary>The handle enter key.</summary>
        protected virtual void HandleEnterKey()
        {
            var line = this.Text.Substring(this.LastPomptIndex);
            this.Text += "\n";
            this.IsInputEnabled = false;
            this.LastPomptIndex = int.MaxValue;

            Command cmd = TerminalUtils.ParseCommandLine(line);
            this.CommandLog.Add(cmd);
            this.indexInLog = this.CommandLog.Count;
            this.RaiseCommandEntered(cmd);
        }

        private string[] RaiseAutoCompleteRequested(Command command)
        {
            if (this.AutoCompleteRequested != null)
            {
                CommandEventArgs args = new CommandEventArgs(command);
                this.AutoCompleteRequested(this, args);
                return args.Command.Args;
            }
            return null;
        }

        /// <summary>The handle tab key.</summary>
        protected virtual void HandleTabKey()
        {
            // Command completion works only if caret is at last character
            // and if the user already typed something.
            if (this.CaretIndex != this.Text.Length || this.CaretIndex == this.LastPomptIndex)
            {
                return;
            }


            // Get command name and associated commands
            var line = lastAutocompleteLine ?? this.Text.Substring(this.LastPomptIndex);
            var commands = this.GetAssociatedCommands(line);

            // If some associated command exist...
            if (commands.Count > 0)
            {
                if (lastSuggestionIndex < commands.Count-1)
                {
                    lastSuggestionIndex++;     
                }
                else
                {
                    lastSuggestionIndex = 0;
                }

                // Erase the user input
                this.Text = this.Text.Remove(this.LastPomptIndex);
                this.Text += commands[lastSuggestionIndex];
                this.CaretIndex = this.Text.Length;

/*
                // Get the commands common prefix
                var commonPrefix = this.GetCommonPrefix(commands);

                // If there is no more autocompletion available...
                if (commonPrefix == line)
                {
                    // If there are more than one command to print
                    if (commands.Length > 1)
                    {
                        // Print every associated command and insert a new prompt
                        foreach (var cmd in commands)
                        {
                            this.Text += "\n" + cmd;
                        }

                        this.InsertNewPrompt();
                        this.Text += line;
                        this.CaretIndex = this.Text.Length;
                    }
                }
                else
                {
                    // Erase the user input
                    this.Text = this.Text.Remove(this.LastPomptIndex);

                    // Insert the common prefix
                    this.Text += commonPrefix;

                    // Set the caret at the end of the text
                    this.CaretIndex = this.Text.Length;
                }
*/

                return;
            }

            // If no command exists, try path completion
            if (line.Contains("\"") && line.Split('"').Length%2 == 0)
            {
                var idx = line.LastIndexOf('"');
                var prefix = line.Substring(0, idx + 1);
                var suffix = line.Substring(idx + 1, line.Length - prefix.Length);
                this.CompletePath(prefix, suffix);
            }
            else
            {
                var idx = Math.Max(line.LastIndexOf(' '), line.LastIndexOf('\t'));
                var prefix = line.Substring(0, idx + 1);
                var suffix = line.Substring(idx + 1, line.Length - prefix.Length);
                this.CompletePath(prefix, suffix);
            }
        }

        /// <summary>The on preview key down.</summary>
        /// <param name="e">The e.</param>
        protected override void OnPreviewKeyDown(KeyEventArgs e)
        {
            // If Ctrl+C is entered, raise an abortrequested event !
            if (e.Key == Key.C)
            {
                if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
                {
                    this.RaiseAbortRequested();
                    e.Handled = true;
                    return;
                }
            }

            // If input is not allowed, warn the user and discard its input.
            if (!this.IsInputEnabled)
            {
                if (this.IsSystemBeepEnabled)
                {
                    SystemSounds.Beep.Play();
                }

                e.Handled = true;
                return;
            }

            // Test the caret position.
            // 1. If located before the last prompt index
            // ==> Warn, set the caret at the end of input text, add text, discard the input
            // if user tries to erase text, else process it.
            // 2. If located at the last prompt index and user tries to erase text
            // ==> Warn, discard the input.
            // 3. If located at the last prompt index and user tries to move backward
            // ==> Warn, discard the input.
            // 4. If located after (>=) the last prompt index and user presses the UP key
            // ==> Launch command history backward, discard the input.
            // 5. If located after (>=) the last prompt index and user presses the UP key
            // ==> Launch command history forward, discard the input.
            if (this.CaretIndex < this.LastPomptIndex)
            {
                if (this.IsSystemBeepEnabled)
                {
                    SystemSounds.Beep.Play();
                }

                this.CaretIndex = this.Text.Length;
                e.Handled = false;
                if (e.Key == Key.Back || e.Key == Key.Delete)
                {
                    e.Handled = true;
                }
            }
            else if (this.CaretIndex == this.LastPomptIndex && e.Key == Key.Back)
            {
                if (this.IsSystemBeepEnabled)
                {
                    SystemSounds.Beep.Play();
                }

                e.Handled = true;
            }
            else if (this.CaretIndex == this.LastPomptIndex && e.Key == Key.Left)
            {
                if (this.IsSystemBeepEnabled)
                {
                    SystemSounds.Beep.Play();
                }

                e.Handled = true;
            }
            else if (this.CaretIndex >= this.LastPomptIndex && e.Key == Key.Up)
            {
                this.HandleCommandHistoryRequest(CommandHistoryDirection.BACKWARD);
                e.Handled = true;
            }
            else if (this.CaretIndex >= this.LastPomptIndex && e.Key == Key.Down)
            {
                this.HandleCommandHistoryRequest(CommandHistoryDirection.FORWARD);
                e.Handled = true;
            }

            // If input has not yet been discarded, test the key for special inputs.
            // ENTER   => validates the input
            // TAB     => launches command completion with registered commands
            // CTRL+C  => raises an abort request event
            if (!e.Handled)
            {
                switch (e.Key)
                {
                    case Key.Enter:
                        this.HandleEnterKey();
                        e.Handled = true;
                        break;
                    case Key.Tab:
                        this.HandleTabKey();
                        e.Handled = true;
                        break;
                }
            }

            if(e.Key != Key.Tab && lastAutocompleteLine != null)
            {
                lastSuggestionIndex = -1;
                lastAutocompleteLine = null;
            }

            base.OnPreviewKeyDown(e);
        }

        // --------------------------------------------------------------------
        // CUSTOM EVENTS
        // --------------------------------------------------------------------

        /// <summary>The raise abort requested.</summary>
        private void RaiseAbortRequested()
        {
            if (this.AbortRequested != null)
            {
                this.AbortRequested(this, new EventArgs());
            }
        }

        /// <summary>The raise command entered.</summary>
        /// <param name="command">The command.</param>
        private void RaiseCommandEntered(Command command)
        {
            if (this.CommandEntered != null)
            {
                this.CommandEntered(this, new CommandEventArgs(command));
            }
        }

        #endregion

        /// <summary>The command event args.</summary>
        public class CommandEventArgs : EventArgs
        {
            #region Constructors and Destructors

            /// <summary>Initializes a new instance of the <see cref="CommandEventArgs"/> class.</summary>
            /// <param name="command">The command.</param>
            public CommandEventArgs(Command command)
            {
                this.Command = command;
            }

            #endregion

            #region Public Properties

            /// <summary>Gets the command.</summary>
            public Command Command { get; private set; }

            #endregion
        }
    }
}